package org.putrpctorest.resource.impl;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.V1_6;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.TraceClassVisitor;
import org.putrpctorest.container.Container;
import org.putrpctorest.resource.Method;
import org.putrpctorest.resource.Parameter;
import org.putrpctorest.resource.Resource;
import org.putrpctorest.service.impl.copy.ServiceInvokerImpl;



public class ServiceClassGeneratorImpl  {

    private Container container;
	
	public ServiceClassGeneratorImpl(Container container) {
		this.container = container;
	}

	private String className;
	private static List<String> methodNames = new ArrayList<String>();
	private static List<String> classNames = new ArrayList<String>();

	
	public Container getContainer() {
		return container;
	}

	public Class<?> generateClass(Container container) {
		//return generateClassForServiceDefinitions(container.getServices());
	    return null;
	}
	
	private Class<?> generateClassForServiceDefinitions() {
	//	String className = getClassName(restServices);
		ClassWriter restClassWriter1 = new ClassWriter(ClassWriter.COMPUTE_MAXS
				+ ClassWriter.COMPUTE_FRAMES);

		TraceClassVisitor restClassWriter = new TraceClassVisitor(
				restClassWriter1, new PrintWriter(System.out));

		// CheckClassAdapter restClassWriter = new
		// CheckClassAdapter(restClassWriter2);

		// CheckClassAdapter cc = new CheckClassAdapter(restClassWriter);
		// ClassVisitor tv = new TraceClassVisitor(cc, );
		// cc.verify(, true, new PrintWriter(System.out));

		restClassWriter.visit(V1_6, ACC_PUBLIC,
				"org/cxfextension/generatedclasses/" + className, null,
				"java/lang/Object", null);
		
		AnnotationVisitor path = restClassWriter.visitAnnotation(
				Type.getDescriptor(Path.class), true);
		path.visit("value", "/");
		path.visitEnd();

		generateConstructor(restClassWriter);
//		for (Resource resource : restServices.getResources()) {
//			for (Method method : resource.getMethods()) {
//				generateMethod(restClassWriter, resource, method);
//			}
//		}
		restClassWriter.visitEnd();
		
		return loadClass(restClassWriter1.toByteArray());
	}

	private void generateConstructor(TraceClassVisitor restClassWriter) {
		MethodVisitor methodVisitor = restClassWriter.visitMethod(ACC_PUBLIC,
				"<init>", "()V", null, null);

		methodVisitor.visitCode();
		methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
		methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
				Type.getInternalName(Object.class), "<init>", "()V");
		methodVisitor.visitInsn(Opcodes.RETURN);
		methodVisitor.visitMaxs(0, 0);
		methodVisitor.visitEnd();

	}

	private void generateMethod(ClassVisitor restClassWriter,
			Resource resource, Method method) {
		int paramCount = method.getParameters().size();
		String methodName = getMethodName(resource, method);
		String methodSignature = getMethodSignature(resource, method);

		MethodVisitor methodVisitor = restClassWriter.visitMethod(ACC_PUBLIC,
				methodName, methodSignature, null,
				new String[] { "java/lang/Exception" });

		AnnotationVisitor get = methodVisitor.visitAnnotation(
				Type.getDescriptor(GET.class), true);
		get.visitEnd();

		AnnotationVisitor path = methodVisitor.visitAnnotation(
				Type.getDescriptor(Path.class), true);
		path.visit("value", resource.getPath());
		path.visitEnd();

		
		AnnotationVisitor produces = methodVisitor.visitAnnotation(
				Type.getDescriptor(Produces.class), true);
		AnnotationVisitor array = produces.visitArray("value");
		for (String producesStr : getProduces(resource, method)) {
			array.visit("", producesStr);
		}
		array.visitEnd();
		produces.visitEnd();

		AnnotationVisitor consumes = methodVisitor.visitAnnotation(
				Type.getDescriptor(Consumes.class), true);
		array = consumes.visitArray("value");
		for (String consumesStr : getConsumes(resource, method)) {
			array.visit("", consumesStr);
		}
		array.visitEnd();
		consumes.visitEnd();

		int i=0;
		for (Parameter parameter:method.getParameters()) {
			Class<?> annotation =parameter.getType()
					.getParameterAnnotation();

			AnnotationVisitor paramAnnotationVisitor = methodVisitor
					.visitParameterAnnotation(i,
							Type.getDescriptor(annotation), true);
			if (annotation.isAssignableFrom(PathParam.class)
					|| annotation.isAssignableFrom(QueryParam.class)
					|| annotation.isAssignableFrom(MatrixParam.class)
					|| annotation.isAssignableFrom(HeaderParam.class)
					|| annotation.isAssignableFrom(CookieParam.class)
					|| annotation.isAssignableFrom(FormParam.class)) {
				paramAnnotationVisitor.visit("value", parameter.getName());
			}
			paramAnnotationVisitor.visitEnd();
			i++;
		}

		methodVisitor.visitCode();
		methodVisitor.visitTypeInsn(Opcodes.NEW,
				Type.getInternalName(HashMap.class));
		methodVisitor.visitInsn(Opcodes.DUP);
		methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
				Type.getInternalName(HashMap.class), "<init>", "()V");
		methodVisitor.visitVarInsn(Opcodes.ASTORE, paramCount + 1);

		for (Parameter parameter: method.getParameters()) {
			methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 1);
			methodVisitor.visitLdcInsn(parameter.getName());
			methodVisitor.visitVarInsn(Opcodes.ALOAD, i + 1);
			methodVisitor.visitMethodInsn(Opcodes.INVOKEINTERFACE,
					"java/util/Map", "put", Type.getMethodDescriptor(
							Type.getType(Object.class),
							new Type[] { Type.getType(Object.class),
									Type.getType(Object.class) }));
			methodVisitor.visitInsn(Opcodes.POP);
		}

		methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, Type
				.getDescriptor(ServiceInvokerImpl.class), "getInstance", Type
				.getMethodDescriptor(Type.getType(ServiceInvokerImpl.class),
						new Type[] {}));
		methodVisitor.visitVarInsn(Opcodes.ASTORE, paramCount + 2);
		methodVisitor.visitLdcInsn(getMethodGlobalId(method));
		methodVisitor.visitVarInsn(Opcodes.ASTORE, paramCount + 3);
		methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 2);
		methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 1);
		methodVisitor.visitVarInsn(Opcodes.ALOAD, paramCount + 3);
		methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type
				.getDescriptor(ServiceInvokerImpl.class), "invokeService", Type
				.getMethodDescriptor(Type.getType(Object.class), new Type[] {
						Type.getType(Map.class), Type.getType(String.class) }));
		methodVisitor.visitInsn(Opcodes.ARETURN);
		methodVisitor.visitMaxs(0, 0);
		methodVisitor.visitEnd();

	}

	private String getMethodGlobalId(Method method) {
//		return method.getResource().getServicesDefinitions().getId() + ":"
//				+ method.getResource().getId() + ":" + method.getId();
	    return null;
	}

	private String getMethodSignature(Resource resource, Method method) {
		Type[] paramTypes = new Type[method.getParameters().size()];

		int i=0;
		for (Parameter parameter:method.getParameters()) {
			if (parameter.getJavaType() != String.class) {
				throw new IllegalStateException(
						"Only Strings are allowed as parameters..");
			}
			paramTypes[i] = Type.getType(parameter.getJavaType());
			i++;
		}
		return Type.getMethodDescriptor(Type.getType(Object.class), paramTypes);
	}

	public static String getMethodName(Resource resource, Method method) {
		String firstProposalForMethodName = getJavaIdentifierForName("service_method_"
				+ resource.getId() + "_" + method.getId());
		StringBuffer proposedMethodName = new StringBuffer(
				firstProposalForMethodName);
		while (methodNames.contains(firstProposalForMethodName)) {
			proposedMethodName.append('_');
		}

		return proposedMethodName.toString();
	}

	public static String getClassName() {

//		String firstProposalForClassName = getJavaIdentifierForName("RestServiceClass"
//				+ restServices.getId());
//		StringBuffer proposedClassName = new StringBuffer(
//				firstProposalForClassName);

//		while(classNames.contains(proposedClassName.toString())) {
//			proposedClassName.append('_');
//		}
//		while (classAlreadyExists(proposedClassName.toString())) {
//			proposedClassName.append('_');
//		}
//		classNames.add(firstProposalForClassName.toString());
//		return proposedClassName.toString();
		return null;
	}

	private static String getJavaIdentifierForName(String name) {
		StringBuffer javaIdentifier = new StringBuffer();

		for (int i = 0; i < name.length(); i++) {
			if (Character.isJavaIdentifierPart(name.charAt(i))) {
				javaIdentifier.append(name.charAt(i));
			} else {
				javaIdentifier.append('_');
			}
		}

		return javaIdentifier.toString();

	}

	private static boolean classAlreadyExists(String className) {

		try {
			Class.forName(className);
			return true;
		} catch (ClassNotFoundException e) {
			return false;
		}
	}

	private Class<?> loadClass(byte[] b) {
		// override classDefine (as it is protected) and define the class.
		Class<?> clazz = null;
		try {
			ClassLoader loader = ClassLoader.getSystemClassLoader();
			Class<?> cls = Class.forName("java.lang.ClassLoader");
			java.lang.reflect.Method method = cls.getDeclaredMethod(
					"defineClass", new Class[] { String.class, byte[].class,
							int.class, int.class });

			// protected method invocaton
			method.setAccessible(true);
			try {
				Object[] args = new Object[] { className, b, new Integer(0),
						new Integer(b.length) };
				clazz = (Class<?>) method.invoke(loader, args);
			} finally {
				method.setAccessible(false);
			}
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}
		return clazz;
	}

	private String[] getConsumes(Resource resource, Method method) {

		Set<String> allConsumes = new HashSet<String>();

		for (String resourceConsumes : resource.getConsumes()) {
			allConsumes.add(resourceConsumes);
		}

		for (String methodConsumes : method.getConsumes()) {
			allConsumes.add(methodConsumes);
		}

		return allConsumes.toArray(new String[0]);
	}

	private String[] getProduces(Resource resource, Method method) {

		Set<String> allProduces = new HashSet<String>();

		for (String resourceProduces : resource.getProduces()) {
			allProduces.add(resourceProduces);
		}

		for (String methodProduces : method.getProduces()) {
			allProduces.add(methodProduces);
		}

		return allProduces.toArray(new String[0]);
	}

}
